﻿/*********************************  Header ********************************\
Author:       Michal Jankowski - www.mjankowski.org
Project:      MJankowski.org.AutoMock
Copyright:    (c)2015, mjankowski.org

This source is subject to the Microsoft Public License.
See http://www.microsoft.com/en-us/openness/licenses.aspx.
All other rights reserved.

THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
\***************************************************************************/

using System;
using System.Linq;
using System.Reflection;
using MJankowski.org.AutoMock.Internals;

namespace MJankowski.org.AutoMock
{
    /// <summary>
    /// Builds TTargetType and fills all its dependencies with mocks.
    /// </summary>
    /// <typeparam name="TTargetType">Type of the object to build.</typeparam>
    public class AutoMock<TTargetType> where TTargetType : class
    {
        private readonly AutoMockCore<TTargetType> TargetBuilder;

        private bool _targetResolved;

        /// <summary>
        /// By default for all dependencies injects substitutes created by NSubstitute framework.
        /// </summary>
        public AutoMock()
        {
            TargetBuilder = new AutoMockCore<TTargetType>();
        }

        /// <summary>
        /// Sets dependencies to be injected while building TTargetType.
        /// </summary>
        public AutoMock(DependencyContainer dependencyContainer)
        {
            TargetBuilder = new AutoMockCore<TTargetType>(dependencyContainer);
        }

        /// <summary>
        /// Sets factory for creating custom mocks.
        /// </summary>
        /// <param name="mockingFactory"></param>
        public AutoMock(IMockingFactory mockingFactory)
        {
            TargetBuilder = new AutoMockCore<TTargetType>(mockingFactory);            
        }

        /// <summary>
        /// Sets factory for creating custom mocks.
        /// Sets dependencies to be injected while building TTargetType.
        /// </summary>
        public AutoMock(IMockingFactory mockingFactory, DependencyContainer dependencyContainer)
        {
            TargetBuilder = new AutoMockCore<TTargetType>(mockingFactory, dependencyContainer);            
        }

        /// <summary>
        /// Instantiates TTargetType with no dependencies. Probably there are better ways to do it ;)
        /// </summary>
        /// <returns>TTargetType instance.</returns>
        /// <exception cref="InvalidOperationException">TTargetType dos not implement default constructor.</exception>
        public TTargetType BuildTargetWithDefaultConstructor()
        {
            return BuildTargetWithSelectedConstructor(constructorInfos =>
            {
                var constructors = constructorInfos
                   .Where(ctorInfo => ctorInfo.GetParameters().None())
                   .ToArray();

                if (constructors.None())
                    throw new InvalidOperationException(String.Format("Object {0} does not contain default constructor.", TargetBuilder.TargetType));

                return constructors.Single();
            });
        }

        /// <summary>
        /// Finds single constructor with parameters.
        /// Instantiates TTargetType and fills it with dependencies.
        /// Uses provided dependencies or creates mocks.
        /// </summary>
        /// <returns>TTargetType instance.</returns>
        /// <exception cref="InvalidOperationException">TTargetType contains more then 1 or no constructors with parameters.</exception>
        public TTargetType BuildTarget()
        {
            return BuildTargetWithSelectedConstructor(constructorInfos =>
            {
                var constructors = constructorInfos
                    .Where(ctorInfo => ctorInfo.GetParameters().Any())
                    .ToArray();

                if (constructors.None())
                    throw new InvalidOperationException(String.Format("Object {0} does not contain constructor with dependencies.", TargetBuilder.TargetType));

                if (constructors.Count() > 1)
                    throw new InvalidOperationException(String.Format("Object {0} contains more than one constructor with dependencies.", TargetBuilder.TargetType));

                return constructors.Single();
            });
        }

        /// <summary>
        /// Finds single constructor with parameters.
        /// Instantiates TTargetType and fills it with dependencies.
        /// Uses provided dependencies or creates mocks.
        /// 
        /// Constructor is selected by number of its parametes.
        /// </summary>
        /// <param name="dependenciesNumber">Number of parameters in constructor.</param>
        /// <returns>TTargetType instance.</returns>
        /// <exception cref="InvalidOperationException">TTargetType contains more then 1 or no constructors with selected parameters number.</exception>
        public TTargetType BuildTarget(int dependenciesNumber)
        {
            return BuildTargetWithSelectedConstructor(constructorInfos =>
            {
                var constructors = constructorInfos
                    .Where(ctorInfo => ctorInfo.GetParameters().Count() == dependenciesNumber)
                    .ToArray();

                if (constructors.None())
                    throw new InvalidOperationException(String.Format("Object {0} does not contain constructor with {1} dependencies.", TargetBuilder.TargetType, dependenciesNumber));

                if (constructors.Count() > 1)
                    throw new InvalidOperationException(String.Format("Object {0} contains more than one constructor with {1} dependencies.", TargetBuilder.TargetType, dependenciesNumber));

                return constructors.Single();
            });
        }

        /// <summary>
        /// Finds single constructor with parameters.
        /// Instantiates TTargetType and fills it with dependencies.
        /// Uses provided dependencies or creates mocks.
        /// </summary>
        /// <param name="selectConstructorFunc">Function for selecting constructor for building TTargetType</param>
        /// <returns>TTargetType instance.</returns>
        /// <exception cref="ArgumentNullException">for null selectConstructorFunc.</exception>
        public TTargetType BuildTargetWithSelectedConstructor(Func<ConstructorInfo[], ConstructorInfo> selectConstructorFunc)
        {
            var targetInstance = TargetBuilder.BuildTargetWithSelectedConstructor(selectConstructorFunc);

            _targetResolved = true;

            return targetInstance;
        }



        /// <summary>
        /// While creating target mocks are created for not provided dependencies.
        /// This method returns created mock of type T.
        /// </summary>
        /// <typeparam name="T">Type of mock to be returned.</typeparam>
        /// <returns></returns>
        public T GetMock<T>() where T : class
        {
            if (!_targetResolved)
                throw new InvalidOperationException("Target was not resolved. There are no mocks created.");

            var type = typeof(T);

            return (T)TargetBuilder.MocksContainer.GetDependency(type);
        }

        /// <summary>
        /// While creating target mocks are created for not provided dependencies.
        /// This method returns created mock of type T.
        /// </summary>
        /// <param name="propertyName">When constructor has more then one parameter of the same type, then you need to provide parameter name to retrieve the mock.</param>
        /// <typeparam name="T">Type of mocks to be returned.</typeparam>
        /// <returns></returns>
        public T GetMock<T>(string propertyName) where T : class
        {
            if (!_targetResolved)
                throw new InvalidOperationException("Target was not resolved. There are no mocks created.");

            var type = typeof(T);

            return TargetBuilder.MocksContainer.GetDependency(type, propertyName) as T;
        }
    }
}
